本文将介绍 2024 年发表在 arXiv 上的论文《DITTO:Elastic Confidential VMs with Secure and Dynamic CPU Scaling》。

解决的问题

机密虚拟机(CVM)在带来了强大的机密性和完整性保护的同时,也带来了很多限制,导致虚拟机的性能和灵活性的下降。例如:不支持 vCPU 的热插拔(即运行中动态调整 vCPU 的数量),该特性可以用于在虚拟机运行过程中灵活调整计算能力,应用于 Serverless 等计算环境下。

虽然的商用 CVM 方案还没有任何一家支持 vCPU 热插拔,但是内存的动态调整是可行的。例如 AMD SEV-SNP 下 hypervisor 可以使用 RMPUPDATE 指令将 CVM 的内存进行回收和动态分配。

由于缺少了 vCPU 数量的动态调整能力,现有的机密无服务器环境(OpenWhisk + Kubernetes + 机密容器)要想动态调整运算能力,只能借助于启动新的 CVM,这会带来很大的性能开销。本文提出了“弹性 CVM” 和 “Woker vCPU” 的概念,能够在 CVM 环境下动态调整参与计算的 vCPU 数量。具体来说,本文的主要贡献如下:

  • 弹性 CVM 的概念: 利用 CVM 和 hypervisor 的协同来动态调整 CPU 资源的分配,增强 CVM 的效率。
  • 创新的 Worker vCPU 设计: Worker vCPU 是一种特殊的 vCPU,通过与 hypervisor 协同的方式被调度,能够随着工作负载的变化而在休眠和工作状态间转变。
  • Worker vCPU 抽象层: 用于简化对 Worker vCPU 的操作。
  • Ditto 原型开发和实验评估: Ditto 是使用了 Worker vCPU 设计的机密 Serverless 平台,能够实现安全且自动扩展的 Serverless 环境。实验评估表明 Ditto 在资源利用上相较于现有的机密 Serverless 平台有显著提升。

设计与实现

Worker vCPU

Woker vCPU 的架构图如下所示:

Worker vCPU 的状态分为活跃和休眠,设计目标是根据系统负载的变化而动态改变 Woker vCPU 的状态。

为了实现的简单和减少 CVM 内 TCB(可信计算基) 的大小,作者选择将对 Worker vCPU 的调度策略放在了 hypervisor 中,而不是由 CVM 内部来决定。而 hypervisor 对 CVM 内部执行情况的了解是很有限的,因此 Worker vCPU 的调度需要 CVM 与 hypervisor 在不损害安全性的前提下完成,且适用的场景没有那么广。主要适用场景为:事件驱动系统和生产者-消费者模型(无状态和松耦合线程),例如 HTTP 请求的发送和处理、数据库查询的请求和处理。这些请求的处理相对独立,能够动态调整计算资源。

初始化 CVM 时,需要指定普通的 vCPU 数量 $m$ 和最大的 Woker vCPU 数量 $n$ ,Woker vCPU 在 CPU 硬件看来与 vCPU 无异,但是为了实现动态运行时调整,CVM 内核和 hypervisor 都必须能够对此进行区分。例如,可以将 $vCPU[1, m]$ 看作是普通 vCPU, $vCPU[m + 1, m + n]$ 看作是 Woker vCPU,CVM 在启动应用时将特定的工作线程绑定到特定的 Woker vCPU 上。

由于 CVM 下虚拟机内部的运行状态对 hypervisor 来说是不可见的(SEV 的内存加密和 SEV-ES 的寄存器状态加密),因此 CVM 与 hypervisor 的协同很重要。例如:Worker vCPU 可以在执行完一个任务后,主动向 hypervisor 发送 “check-in” 的信号,表明一个任务已完成,hypervisor 受到信号后便可以决定要不要将 Worker vCPU 置为休眠状态。之所以需要这样的协同,是为了防止 hypervisor 在一个任务执行中途将 Worker vCPU 置为休眠,导致任务执行被推迟。

Ditto

Ditto 的架构图如下所示:

Ditto 相较于传统的 Kata 容器的部署过程,存在下列主要不同:

  • 启动 CVM: 以 $m$ 个普通 vCPU 和 $n$ 个 Worker vCPU 进行初始化。
  • Worker vCPU 注册: 应用启动时需要将特定的线程注册到 Worker vCPU 上,且保持不变,同时还不允许其运行内核函数,因此 Worker vCPU 可以被安全的启用和睡眠,不会影响整体系统的正常工作,只会影响效率。

Worker vCPU 调度

调度器依赖于两个方面: 观察指标和调度算法

在 CVM 环境下,hypervisor 的观察指标很有限。在 Ditto 中,作者主要基于 Linux 内核数据结构 task_struct 的时间信息来计算一个采样周期内 vCPU 运行的时间来计算工作负载量。

这里信息的获取方式不太明白。

其他一些可能的指标还有:HTTP 请求的数量、每个请求的近似处理时间等。

调度算法方面,作者采用了一个简单的策略:当活跃的 vCPU 的总负载达到一个预先设定的阈值后,就唤醒一个 Worker vCPU,活跃的 vCPU 的总负载降低过一个预先设定的阈值后,就睡眠一个 Worker vCPU。注意如前文所述,需要在接收到 CHECKIN 请求后才能睡眠,防止中断任务执行。如果所有的 Worker vCPU 都不足以应对工作负载,可以考虑启动新的 CVM。

运行时控制

为了方便实现对 Worker vCPU 的动态控制,需要定义一套的 CVM-hypervisor 通信协议。通信基于预先定义的 CPUID 实现。在 SEV-ES 中,CPUID 指令会触发 #VC 异常,会陷入到 guest 内核的 VC 处理程序中,执行必要的检查,并将需要向 hypervisor 提供的信息(信息类型和信息参数等)通过共享的 GHCB 内存块传递,并退出虚拟机回到 hypervisor 进行处理。下图是作者定义的一个简单的通讯协议: